classdef dotsQueryableEyeASL < dotsQueryableEye
    % @class dotsQueryableEyeASL
    % Acquires and classifies ASL/serial port gaze and pupil size data.
    % @details
    % dotsQueryableEyeASL extends the dotsQueryableEye superclass to
    % acquire point of gaze and pupil size data from an ASL eye tracker
    % connected via the serial port.
    % @details
    % It uses the as() mex funciton (which is only for Mac OS X) to buffer
    % and decode the streaming serial port data.  as() makes a number of
    % assumptions about the data format, including that the data frames are
    % 12 bytes and include the frame number.
    % @details
    % dotsQueryableEyeASL is a usable class.  Use the newObject()
    % method of dotsTheQueryablesManager to create new
    % dotsQueryableEyeASL objects.
    % @ingroup queryable
    
    properties
        % any function that returns the current time as a number
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        clockFunction;
        
        % nx2 matrix of ASL frame numbers and clockFunction timestamps
        % @details
        % Each call to readRawData() adds a row to frameTimestamps.  The
        % first column contains the latest frame number returned from as().
        % The second column contains a timestamp from clockFunction.
        % dotsQueryableEyeASL assumes that the numbered frame coincided
        % with the timestamp.
        % @details
        % logAllData() adds the frameTimestamps matrix to topsDataLog.
        % flushData() clears frameTimestamps.
        frameTimestamps = [];
        
        % the largest frame number integer that ASL can represent
        % @details
        % ASL reports frame numbers as integers which overflow
        % periodically.  readRawData uses frameRollover to correct for the
        % overflow, even if frames are dropped.
        frameMaxInt = (2^16)-1;
    end
    
    methods
        % Acquire device-specific resources.
        % @details
        % Initializes the the as() mex function, returns true if
        % successful, otherwise returns false.
        function isOpen = openEyeTracker(self)
            dotsTheSwitchboard.sharePropertyOfObject( ...
                'clockFunction', self);
            
            self.closeEyeTracker;
            isOpen = false;
            if exist('as', 'file') > 0
                try
                    isOpen = as('init') >= 0;
                    
                catch err
                    isOpen = false;
                    warning(err.message);
                end
            end
        end
        
        % Release device-specific resources.
        % @details
        % Closes the as() mex function.
        function closeEyeTracker(self)
            if self.isAvailable
                as('close');
            end
        end
        
        % Clear data from this object and the as() internal buffer.
        % @details
        % Extends the dotsQueryable flushData() method to also clear out
        % the as() internal data buffer and discard old frame timestamps.
        function flushData(self)
            self.flushData@dotsQueryableEye;
            if self.isAvailable
                as('reset');
            end
            self.frameTimestamps = [];
        end
        
        % Add the allData and frameTimestamps to the topsDataLog.
        % @details
        % Extends the dotsQueryable logAllData() method to also log
        % frameTimestamps, using the group
        % name "@e class.frameTimestamps", where @e class is the object's
        % class name.
        function logAllData(self)
            self.logAllData@dotsQueryable;
            groupName = sprintf('%s.frameTimestamps', class(self));
            topsDataLog.logDataInGroup(self.frameTimestamps, groupName);
        end
        
        % Read raw data from the acquired eye tracker device.
        % @details
        % Reads out any buffered data frames from the as() mex function's
        % internal buffer.  Reformats the data in the dotsQueryable style.
        % @details
        % as() reports data frames in four columns as
        %   - [x y pupil frameNumber]
        %   .
        % Converts these to the dotsQueryable three-column style as
        %   - [xID, x, timestamp]
        %   - [yID, y, timestamp]
        %   - [pupilID, pupilSize, timestamp]
        %   .
        % where timestamp is derived from the ASL frameNumber,
        % sampleFrequency, and the current time.
        % @details
        % Appends the latest frameNumber and the current time to
        % frameTimestamps.
        function data = readRawData(self)
            nowTime = feval(self.clockFunction);
            aslFrames = as('read');
            nData = 3*size(aslFrames, 1);
            data = zeros(nData, 3);
            if nData > 0
                % frame numbers to local time values
                frameNumbers = aslFrames(:,4);
                frameTimes = self.computeFrameTimes(frameNumbers, nowTime);
                
                % data to dotsQueryable format
                xIndexes = 1:3:nData;
                data(xIndexes,1) = self.xID;
                data(xIndexes,2) = aslFrames(:,2);
                data(xIndexes,3) = frameTimes;
                
                yIndexes = 2:3:nData;
                data(yIndexes,1) = self.yID;
                data(yIndexes,2) = aslFrames(:,3);
                data(yIndexes,3) = frameTimes;
                
                dIndexes = 3:3:nData;
                data(dIndexes,1) = self.pupilID;
                data(dIndexes,2) = aslFrames(:,1);
                data(dIndexes,3) = frameTimes;
            end
        end
        
        function frameTimes = computeFrameTimes(self, frameNumbers, localTime)
            % save the raw last frame number with its local timestamp
            self.frameTimestamps(end+1,1:2) = ...
                [frameNumbers(end), localTime];
            
            % correct frame numbers for integer overflow
            rollover = diff(frameNumbers) < 0;
            if any(rollover)
                ii = find(rollover, 1) + 1;
                frameNumbers(ii:end) = ...
                    frameNumbers(ii:end) + self.frameMaxInt + 1;
            end
            
            % compute frame times with last frame defined as localTime
            frameNumbers = frameNumbers - frameNumbers(end);
            frameTimes = ...
                localTime + frameNumbers ./ self.sampleFrequency;
        end
    end
    
    methods (Static)
        function frameNumbers = reconstructFrameNumbers(frameTimes, frameTimestamps, sampleFrequency)
            frameNumbers = zeros(size(frameTimes));
            
            nHints = size(frameTimestamps, 1);
            firstFrame = 0;
            lastFrame = 0;
            for ii = 1:nHints
                nowNumber = frameTimestamps(ii,1);
                nowTime = frameTimestamps(ii,2);
                firstFrame = lastFrame + 1;
                lastFrame = find(frameTimes <= nowTime, 1, 'last');
                times = frameTimes(firstFrame:lastFrame);
                frameNumbers(firstFrame:lastFrame) = round( ...
                    ((times - nowTime) * sampleFrequency) + nowNumber);
            end
        end
    end
end